package com.container.app;

import com.cache.app.CacheManager;
import com.cache.app.KCache;
import com.cache.app.exceptions.NoSuchElementInCacheException;
import com.container.app.annotations.ImplementationTag;
import com.container.app.annotations.Inject;
import com.container.app.annotations.InstanceTag;
import com.container.app.exceptions.MContainerUncheckedException;
import com.container.app.proxy.ContainerProxyHandler;
import com.container.app.utilities.*;
import org.jdom.JDOMException;

import java.io.IOException;
import java.lang.annotation.IncompleteAnnotationException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.List;
import java.util.Properties;

import static com.container.app.utilities.ContainerUtils.*;

//import com.cache.app.*;

/**
 * Created with IntelliJ IDEA.
 * User: Georgiana.Scarlat
 * Date: 7/11/12
 * Time: 2:12 PM
 */
public final class MContainerManager implements MContainer {


    /* hashmap with key = pair of a class and a pair of it's annotation class and value
*             value =  implementing class or object instance */
    private KCache<Integer, CacheableMapping> interfaceAnnotationImplementationMapping;

    /* cache that contains the instances of the classes that have been requested through the getInstance() method */
    private KCache<Class, CacheableInstance> instantiations;

    /* cache that contains mappings between classes and their component name*/
    private KCache<String,CacheableClass> classNames;

    private boolean withProxies = false;
    private Class<? extends ContainerProxyHandler> proxyHandlerClass;

    public MContainerManager() {
        classNames = new CacheManager<String, CacheableClass>(null, 5000, 6000, CLASS_MAPPINGS_OUT);
        instantiations = new CacheManager<Class, CacheableInstance>(new InstanceFetcher(this), 5000, 6000, OBJECT_INSTANCES_OUT);
        interfaceAnnotationImplementationMapping = new CacheManager<Integer, CacheableMapping>(null, 5000, 6000, MAPPINGS_OUT);

    }


    public MContainerManager(Class<? extends MContainerConfiguration> configurationClass) throws MContainerUncheckedException {
        this();
        getInfoFromConfigurationClass(configurationClass);


    }


    public MContainerManager(String xmlFileName) throws MContainerUncheckedException {

        this();
        try {
            XmlParser.parseInterfaceAnnotationImplementationMapping(xmlFileName, this);
        } catch (JDOMException e) {
            throw new MContainerUncheckedException("XML configuration file " + xmlFileName + " has errors. ", e);
        } catch (IOException e) {
            throw new MContainerUncheckedException("Can't read XML configuration file " + xmlFileName + ". ", e);
        } catch (ClassNotFoundException e) {
            throw new MContainerUncheckedException("XML configuration file " + xmlFileName + " has errors in class names. ", e);
        }

    }

    public MContainerManager(String propertyFileName, String location) throws MContainerUncheckedException {
        this();
        startContainerFromPropertiesFile(getPath(propertyFileName, location), RunType.ALL);
    }


    @SuppressWarnings("UnusedDeclaration")
    public MContainerManager(String propertyFileName, String location, Class<? extends ContainerProxyHandler> proxyHandler) throws MContainerUncheckedException {
        this();
        startContainerFromPropertiesFile(getPath(propertyFileName, location), RunType.SCAN);
        if(proxyHandler != null) setWithProxies(true, proxyHandler);
        startContainerFromPropertiesFile(getPath(propertyFileName, location), RunType.LOAD);
    }


    public void setWithProxies(boolean withProxies, Class<? extends ContainerProxyHandler> proxyHandler) throws MContainerUncheckedException {

        this.withProxies = withProxies;
        this.proxyHandlerClass = proxyHandler;
        if(withProxies) resolveDependencies(this.proxyHandlerClass, ObjectType.CLASS);
    }


    public void runContainerScan(String propertyFileName) throws MContainerUncheckedException, IncompleteAnnotationException {

        run(loadProperties(propertyFileName), RunType.SCAN);
    }

    /**
     * Scans classes from a package to find mappings for components.
     *
     * @param packageName package name
     */
    public void scanPackage(String packageName) throws MContainerUncheckedException, IncompleteAnnotationException {

        runOnPackage(packageName, RunType.SCAN);
    }


    /**
     * Loads instances from all the classes in a given package into the container's cache.
     * <p/>
     * Resolves all dependencies by recursively injecting them.
     *
     * @param packageName the name of the package
     * @throws com.container.app.exceptions.MContainerUncheckedException
     *
     */
    public void runContainerOnPackage(String packageName) throws MContainerUncheckedException {

        runOnPackage(packageName, RunType.LOAD);
    }

    /**
     * Loads instances from all the classes from the packages specified
     * into the property file into the container's cache.
     * <p/>
     * Resolves all dependencies by recursively injecting them.
     *
     * @param propertyFileName property file name
     * @throws MContainerUncheckedException
     */
    public void runContainer(String propertyFileName) throws MContainerUncheckedException {

        run(loadProperties(propertyFileName), RunType.LOAD);
    }


    /**
     * Obtains an instance of the class given by parameter.
     * <p/>
     * Only classes annotated with @Component are taken into account.
     * If the class has setters annotated with @Inject, the corresponding values
     * are injected through these setters.
     * Returns the same instance every time it is invoked for the same class.
     *
     * @param cls the Class
     * @return an instance of the class or null if the class is not annotated correspondingly
     */
    public <T> T getInstance(Class cls) throws MContainerUncheckedException {

        resolveDependencies(cls, ObjectType.CLASS);

        return getInstanceFromKey(new Pair<Class, Pair<Class, String>>(cls, null));

    }


    public <T> T getInstance(String componentName) throws MContainerUncheckedException {

        CacheableClass componentCacheableClass;
        Class componentClass;

        try {
            componentCacheableClass = classNames.getFromCache(componentName);
            componentClass = componentCacheableClass.getNamedClass();
            return getInstance(componentClass);
        } catch (NoSuchElementInCacheException e) {

            throw new MContainerUncheckedException("There is no component with name "+componentName,e);
        }
    }


    /**
     * Looks for setters annotated with @Inject and injects the
     * corresponding values through these setters.
     *
     * @param instance the instance/class in which we inject
     * @param type     0 if it's an instance we're injecting in, otherwise it's a class
     */
    public <T> void resolveDependencies(T instance, ObjectType type) throws MContainerUncheckedException {

        if (type.equals(ObjectType.CLASS) && !isComponent((Class) instance))
            return;

        Method methods[] = getWantedMethods(instance, type);

        for (Method method : methods) {
            if (method.isAnnotationPresent(Inject.class)) {
                inject(instance, method);
            }
        }
    }


    /**
     * Closes the container. This is necessary in order to stop
     * background threads that are used to clean up the caches.
     */
    public void closeContainer() {

        classNames.closeCache();
        instantiations.closeCache();
        interfaceAnnotationImplementationMapping.closeCache();
    }

    /**
     * Adds an interface-annotation-implementation association to the container's local mapping cache.
     *
     * @param interface_annotation_implementationAssociation
     *         interface-annotation-implementation association
     */
    public void addMappingToCache(Pair<Pair<Class, Pair<Class, String>>, Object> interface_annotation_implementationAssociation) {

        interfaceAnnotationImplementationMapping.putInCache(new CacheableMapping(interface_annotation_implementationAssociation));
    }


    /**
     * Method used to test if a list of mappings are found in the MContainerManager's mappings cache.
     *
     * @param expectedMappings the list of expected mappings
     * @return true if it contains the mappings, false otherwise
     */
    public boolean containsMappings(List<Pair<Pair<Class, Pair<Class, String>>, Object>> expectedMappings) throws NoSuchElementInCacheException {

        for (Pair<Pair<Class, Pair<Class, String>>, Object> mapping : expectedMappings) {
            if (!mapping.getSecond().equals(interfaceAnnotationImplementationMapping.getFromCache(mapping.getFirst().hashCode()).getMapping())) {
                return false;
            }
        }
        return true;
    }


    private <T> T getInstanceFromKey(Pair<Class, Pair<Class, String>> key) {

        T instance = null;

        try {

            CacheableMapping cacheableMapping = interfaceAnnotationImplementationMapping.getFromCache(key.hashCode());

            Class cls = (Class)cacheableMapping.getMapping();

            resolveDependencies(cls, ObjectType.CLASS);
            instance = getInstanceForMapping(key, cls);

        } catch (NoSuchElementInCacheException e) {

            @SuppressWarnings("unchecked") Class<T> keyClass = (Class<T>) key.getFirst();

            if (isInstanciable(keyClass))
                instance = getOnlyInstance(keyClass);
        }

        return instance;

    }

    private <T> T getInstanceForMapping(Pair<Class, Pair<Class, String>> key, Class cls) {

        T instance;
        instance = getInstanceFromKey(new Pair<Class, Pair<Class, String>>(cls, null));

        instance = maybeSingleton(instance);

        addClassNameToCache(instance);

        if (withProxies && instance != null && key.getFirst().isInterface())
            instance = makeProxy(instance, proxyHandlerClass);
        return instance;
    }

    private <T> T getOnlyInstance(Class<T> cls) {

        if (!isComponent(cls))
            return null;

        CacheableInstance cacheableInstance;

        try {
            cacheableInstance = instantiations.getFromCache(cls);
            //noinspection unchecked
            return (T) cacheableInstance.getInstance();
        } catch (NoSuchElementInCacheException e) {
            throw new MContainerUncheckedException("Can't instantiate class " + cls.getName(), e);
        }


    }


    /**
     * Injects an object into an instance using a setter method that is annotated with @Inject.
     *
     * @param instance the instance into which we inject
     * @param method   setter method
     */
    private void inject(Object instance, Method method) throws MContainerUncheckedException {

        Object injectedObject;
        Pair<Class, Pair<Class, String>> key;
        CacheableMapping cacheableMapping;

        key = getMappingBinded(method, ImplementationTag.class);
        injectedObject = getInstanceFromKey(key);
        if (injectedObject == null) {
            key = getMappingBinded(method, InstanceTag.class);
            try {
                cacheableMapping = interfaceAnnotationImplementationMapping.getFromCache(key.hashCode());
                injectedObject = cacheableMapping.getMapping();
            } catch (NoSuchElementInCacheException e1) {
                throw new MContainerUncheckedException("Can't find any mapping to inject through method " + method.getName(), e1);
            }
        }

        if (injectedObject != null)
            useSetter(instance, method, injectedObject);
        else
            throw new MContainerUncheckedException("Can't inject null references.");

    }


    private void getInfoFromConfigurationClass(Class<? extends MContainerConfiguration> configurationClass) throws MContainerUncheckedException {
        Constructor constructor;
        try {
            constructor = configurationClass.getDeclaredConstructor(MContainer.class);
            constructor.setAccessible(true);
            MContainerConfiguration containerConfiguration = (MContainerConfiguration) constructor.newInstance(this);
            containerConfiguration.configure();
        } catch (NoSuchMethodException e) {
            throw new MContainerUncheckedException("Configuration class " + configurationClass + " doesn't have a corresponding constructor that receives a MContainerManager object as a parameter. ", e);
        } catch (Exception e) {
            throw new MContainerUncheckedException("Invalid configuration class " + configurationClass.getName(), e);
        }
    }

    @SuppressWarnings("unchecked")
    private void getInfoFromConfigurationClasses(Properties properties) {
        Enumeration keys = properties.propertyNames();
        try {

            while (keys.hasMoreElements()) {
                String keyName = (String) keys.nextElement();

                if (keyName.startsWith(CONFIGURATION)) {
                    String configClassName = properties.getProperty(keyName);
                    Class configClass = Class.forName(configClassName);
                    if (MContainerConfiguration.class.isAssignableFrom(configClass))
                        getInfoFromConfigurationClass(configClass);
                    else
                        throw new MContainerUncheckedException("Configuration file has invalid configuration class name.");
                }
            }
        } catch (ClassNotFoundException e) {
            throw new MContainerUncheckedException("Configuration file has invalid configuration class name.", e);
        }
    }


    private void run(Properties properties, RunType type) throws MContainerUncheckedException {

        Enumeration keys = properties.propertyNames();
        while (keys.hasMoreElements()) {
            String keyName = (String) keys.nextElement();
            if (keyName.startsWith(PACKAGE)) {
                String packageName = properties.getProperty(keyName);
                switch (type) {
                    case SCAN:
                        scanPackage(packageName);
                        break;
                    case LOAD:
                        runContainerOnPackage(packageName);
                        break;
                }
            }

        }
    }



    private void runOnPackage(String packageName, RunType type) {
        try {
            List<Class> classes = PackageClasses.getClasses(packageName);

            for (Class clas : classes) {
                if (isComponent(clas)) {
                    switch (type) {
                        case SCAN:
                            scanClass(clas);
                            break;
                        case LOAD:
                            //noinspection unchecked
                            getInstance(clas);
                            break;
                    }

                }

            }
        } catch (ClassNotFoundException e) {
            throw new MContainerUncheckedException("There are unloaded classes from package " + packageName, e);
        } catch (IOException e) {
            throw new MContainerUncheckedException("Can't access class files from package " + packageName, e);
        }

    }

    /**
     * Scans a class to add mappings between it's implementing
     * interfaces, it's superclass and itself.
     *
     * @param clas class
     */
    private void scanClass(Class clas) throws IncompleteAnnotationException {

        Class interfaces[] = clas.getInterfaces();

        String componentName = getComponentMethodValue(clas, NAME);

        Pair<Class, String> annotationValue = null;
        if (!componentName.equals(EMPTY_STRING)) {
            annotationValue = new Pair<Class, String>(ImplementationTag.class, componentName);
            putNameInCache(clas);
        }

        for (Class inter : interfaces) {
            addMappingToCache(new Pair<Pair<Class, Pair<Class, String>>, Object>(new Pair<Class, Pair<Class, String>>(inter, annotationValue), clas));
        }

        Class superClass = clas.getSuperclass();
        if (superClass != null)
            addMappingToCache(new Pair<Pair<Class, Pair<Class, String>>, Object>(new Pair<Class, Pair<Class, String>>(superClass, annotationValue), clas));
    }





    private void runDependencies(Properties properties, RunType type) {

        Enumeration keys = properties.propertyNames();

        while (keys.hasMoreElements()) {
            String keyName = (String) keys.nextElement();
            if (keyName.startsWith(DEPENDENCE)) {
                String dependenceName = properties.getProperty(keyName);
                startContainerFromPropertiesFile(dependenceName, type);
            }

        }
    }

    private void startContainerFromPropertiesFile(String propertyFileName, RunType type) {

        Properties properties = loadProperties(propertyFileName);

        runDependencies(properties, type);
        switch (type) {
            case SCAN:
                getInfoFromConfigurationClasses(properties);
                run(properties, RunType.SCAN);
                break;
            case LOAD:
                run(properties, RunType.LOAD);
                break;
            case ALL:
                getInfoFromConfigurationClasses(properties);
                run(properties, RunType.SCAN);
                run(properties, RunType.LOAD);
                break;
        }
    }

    private <T> void addClassNameToCache(T instance) {

        if(instance == null)
            return;

        Class<?> cls = instance.getClass();

        if(!EMPTY_STRING.equals(getComponentMethodValue(cls, NAME)))
            putNameInCache(cls);

    }

    private void putNameInCache(Class<?> cls) {
        classNames.putInCache(new CacheableClass(cls));
    }


    @Override
    public String toString() {
        return "MContainerManager{" +
                "interfaceAnnotationImplementationMapping=" + interfaceAnnotationImplementationMapping +
                '}';
    }

}
